<!DOCTYPE html>
<html>
<head>
    <title>Example Chunked Uploader</title>
</head>
<body>

<h1>Example Chunked Uploader</h1>

<p>This is some example JavaScript code that uploads a file in chunks in the format expected by the nginx module.</p>

<p>It is probably possible to simply copy the "doUpload" function into your code but not recommended (error handling isn't included, "alert" is used and the chunk size is hard coded).</p>

<p>Please open browser's developer console to see detailed logs.</p>

<input id="file-to-upload" type="file"><button id="upload-button">Upload</button>

<div id="files">

</div>


<script>

function doUpload(url, extraParams, sessionID, file, progress, success) {
    var chunkSize = 32000,// this is first chunk size, which will be adaptively expanded depending on upload speed
        aborting = false,
        TO = null,
        lastChecksum = null,
        offset = 0,
        lastChunkTime = 4000; //used for adaptive chunk size recalculation
    var xhr;

    function adjustChunkSize(startTime) {
        var end = new Date().getTime();
        var duration = end - startTime;
        var kbps = Math.round(chunkSize / duration);
        if (duration < 2000 && chunkSize < 8 * 1024 * 1024) {
            //faster, increase chunk size up to 8MB, there will be no more increases over 4MB/s
            // Important: remember about client_max_body_size in nginx config if you alter max chunk size
            chunkSize <<= 1;
            console.log("Increase chunkSize="+chunkSize+", kbps="+kbps);
        } else if (duration > 10000 && chunkSize > 32 * 1024) {
            //slower, down to min 32KB chunk size, there will be no more decreases below ~3.2KB/s
            chunkSize >>= 1;
            console.log("Increase chunkSize="+chunkSize+", kbps="+kbps);

        }
    }

    var uploadNextChunk = function() {
            TO = null;
            var chunkStartTime = new Date().getTime();

            var chunkStart = offset,
                chunkEnd = Math.min(offset + chunkSize, file.size) - 1;

            var currentBlob = (file.slice || file.mozSlice || file.webkitSlice).call(file, chunkStart, chunkEnd + 1);

            if (!(currentBlob && currentBlob.size > 0)) {
                alert('Chunk size is 0'); // Sometimes the browser reports an empty chunk when it shouldn't, could retry here
                return;
            }


            xhr = new XMLHttpRequest();

            if (chunkEnd === file.size - 1) {
                // Add extra URL params on the last chunk
                // Important: URL parameters passing this doesn't work currently, pass parameters via some custom "X-" header instead
                xhr.open('POST', url + (url.indexOf('?') > -1 ? '&' : '?') + extraParams, true);
            } else {
                xhr.open('POST', url, true);
            }

            // chunking gives usually ~2sec progress you can skip this handler
            xhr.upload.addEventListener('progress', function(e) {
                if (aborting) {
                    return;
                }

                progress((chunkStart + e.loaded) / file.size);
            });

            xhr.addEventListener('load', function() {
                if (aborting) {
                    return;
                }

                if (xhr.readyState >= 4) {
                    if (xhr.status === 200) {
                        progress((chunkEnd + 1) / file.size);

                        // done
                        success(xhr.responseText);

                    } else if (xhr.status === 201) {
                        offset = chunkEnd + 1;
                        progress(offset / file.size);
                        adjustChunkSize(chunkStartTime);

                        //the checksum of data uploaded so far
                        lastChecksum = xhr.getResponseHeader('X-Checksum');
                        TO = setTimeout(uploadNextChunk, 1); // attempt to avoid xhrs sticking around longer than needed
                    } else {
                        // error, restart chunk
                        try {
                            xhr.abort();
                        } catch (err) {}

                        alert('A server error occurred: ' + xhr.responseText); // Could retry at this stage depending on xhr.status
                    }
                }
            });

            xhr.addEventListener('error', function() {
                if (aborting) {
                    return;
                }

                // error, restart chunk

                try {
                    xhr.abort();
                } catch (err) {}

                alert('A server error occurred: ' + xhr.responseText); // Could retry at this stage depending on xhr.status
            });

            xhr.setRequestHeader('Content-Type', 'application/octet-stream');
            xhr.setRequestHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(file.name) + '"');
            xhr.setRequestHeader('X-Content-Range', 'bytes ' + chunkStart + '-' + chunkEnd + '/' + file.size);
            xhr.setRequestHeader('X-Session-ID', sessionID);
            if(lastChecksum) {
                //client must pass the checksum of all previous chunks
                //this way server will continue checksum calculation
                xhr.setRequestHeader('X-Last-Checksum', lastChecksum);
            }
            xhr.send(currentBlob);
        };

    TO = setTimeout(uploadNextChunk, 1);


    return {
        abort: function() {
            aborting = true;
            if (TO !== null) {
                clearTimeout(TO);
                TO = null;
            }
            try {
                xhr.abort();
            } catch (err) {}
        },
        pause: function() {
            this.abort();
            aborting = false;
        },
        resume: function() {
            uploadNextChunk();
        }
    };
}


//naive body param value extractor
function getParam(body, name) {
    var params = body.split('&');
    for(var idx in params) {
        var match = params[idx].match(new RegExp('^'+name+'=(.+)$'));
        if(match && match.length > 1) {
          return match[1];
        }
    }
}


// Feature detection for required features
if (!!((typeof(File) !== 'undefined') && (typeof(Blob) !== 'undefined') && (typeof(FileList) !== 'undefined') && (Blob.prototype.webkitSlice|| Blob.prototype.mozSlice || Blob.prototype.slice))) {

    document.getElementById('upload-button').addEventListener('click', function() {

        var file = document.getElementById('file-to-upload').files[0];

        //generate random long number for SessionID
        var sessionID = Math.round(Math.pow(10,17)*Math.random());

        doUpload('/upload', 'test=1', sessionID, file, function(progress) {

            console.log('Total file progress is ' + Math.floor(progress * 100) + '%');

        }, function(responseText) {

            var backendFileId = getParam(responseText, "id");
            var backendFileName = getParam(responseText, "name");
            var backendFileSize = getParam(responseText, "size");
            console.log('Success - server responded with:', responseText);


            var div = document.createElement('div');
            var link = document.createElement('a');
            link.setAttribute('href', '/file/'+ backendFileId + "/" +backendFileName);
            link.setAttribute('target', '_blank');
            link.appendChild(document.createTextNode(backendFileName + " ("+backendFileSize+")"));
            div.appendChild(link);
            document.getElementById("files").appendChild(div);

        });

    }, false);

} else {

    alert('Your browser does not support chunked uploading');

}



</script>

</body>
</html>
